Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.84% covered (success)
93.84%
198 / 211
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
AddDoctrineFields
93.84% covered (success)
93.84%
198 / 211
50.00% covered (danger)
50.00%
2 / 4
56.73
0.00% covered (danger)
0.00%
0 / 1
 postRun
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 applyId
82.76% covered (warning)
82.76%
48 / 58
0.00% covered (danger)
0.00%
0 / 1
20.85
 iterateProperties
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 patch
97.93% covered (success)
97.93%
142 / 145
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2namespace Apie\DoctrineEntityConverter\CodeGenerators;
3
4use Apie\Core\Context\ApieContext;
5use Apie\Core\Entities\RequiresRecalculatingInterface;
6use Apie\Core\Identifiers\AutoIncrementInteger;
7use Apie\Core\Metadata\MetadataFactory;
8use Apie\Core\Utils\ConverterUtils;
9use Apie\DoctrineEntityConverter\Concerns\HasGeneralDoctrineFields;
10use Apie\DoctrineEntityConverter\Concerns\RequiresDomainUpdate;
11use Apie\DoctrineEntityConverter\Entities\SearchIndex;
12use Apie\StorageMetadata\Attributes\AclLinkAttribute;
13use Apie\StorageMetadata\Attributes\DecimalPropertyAttribute;
14use Apie\StorageMetadata\Attributes\DiscriminatorMappingAttribute;
15use Apie\StorageMetadata\Attributes\GetMethodAttribute;
16use Apie\StorageMetadata\Attributes\GetSearchIndexAttribute;
17use Apie\StorageMetadata\Attributes\ManyToOneAttribute;
18use Apie\StorageMetadata\Attributes\OneToManyAttribute;
19use Apie\StorageMetadata\Attributes\OneToOneAttribute;
20use Apie\StorageMetadata\Attributes\OrderAttribute;
21use Apie\StorageMetadata\Attributes\ParentAttribute;
22use Apie\StorageMetadata\Attributes\PropertyAttribute;
23use Apie\StorageMetadata\Interfaces\AutoIncrementTableInterface;
24use Apie\StorageMetadataBuilder\Interfaces\MixedStorageInterface;
25use Apie\StorageMetadataBuilder\Interfaces\PostRunGeneratedCodeContextInterface;
26use Apie\StorageMetadataBuilder\Mediators\GeneratedCodeContext;
27use Apie\TypeConverter\ReflectionTypeFactory;
28use Doctrine\Common\Collections\Collection;
29use Doctrine\ORM\Mapping\Column;
30use Doctrine\ORM\Mapping\Entity;
31use Doctrine\ORM\Mapping\GeneratedValue;
32use Doctrine\ORM\Mapping\HasLifecycleCallbacks;
33use Doctrine\ORM\Mapping\Id;
34use Doctrine\ORM\Mapping\JoinColumn;
35use Doctrine\ORM\Mapping\ManyToMany;
36use Doctrine\ORM\Mapping\ManyToOne;
37use Doctrine\ORM\Mapping\OneToMany;
38use Doctrine\ORM\Mapping\OneToOne;
39use Doctrine\ORM\Mapping\OrderBy;
40use Generator;
41use Nette\PhpGenerator\Attribute;
42use Nette\PhpGenerator\ClassType;
43use Nette\PhpGenerator\PromotedParameter;
44use Nette\PhpGenerator\Property;
45use ReflectionClass;
46use ReflectionProperty;
47
48/**
49 * Adds created_at and updated_at and Doctrine attributes
50 */
51class AddDoctrineFields implements PostRunGeneratedCodeContextInterface
52{
53    public function postRun(GeneratedCodeContext $generatedCodeContext): void
54    {
55        foreach ($generatedCodeContext->generatedCode->generatedCodeHashmap as $code) {
56            $this->patch($generatedCodeContext, $code);
57        }
58    }
59
60    private function applyId(ClassType $classType): void
61    {
62        $property = null;
63        $doctrineType = null;
64        $nullable = false;
65        $generatedValue = false;
66        if ($classType->hasProperty('id')) {
67            $property = $classType->getProperty('id');
68        } elseif ($classType->hasProperty('search_id')) {
69            $property = $classType->getProperty('search_id')->cloneWithName('id');
70            $classType->addMember($property);
71        }
72        if ($property === null) {
73            $property = $classType->addProperty('id')->setType('?int');
74            $generatedValue = true;
75            $doctrineType = 'integer';
76        } else {
77            // @see ClassTypeFactory
78            $originalClass = $classType->getComment();
79            if ($originalClass && class_exists($originalClass)) {
80                $metadata = MetadataFactory::getResultMetadata(
81                    new ReflectionClass($originalClass),
82                    new ApieContext()
83                );
84                $hashmap = $metadata->getHashmap();
85                if (isset($hashmap['id'])) {
86                    $type = $hashmap['id']->getTypehint();
87                    $nullable = $hashmap['id']->allowsNull();
88                    $class = ConverterUtils::toReflectionClass($type);
89                    if ($class && $class->isSubclassOf(AutoIncrementInteger::class)) {
90                        $generatedValue = true;
91                        $nullable = false;
92                        $property->setInitialized(true);
93                    }
94                    $scalarType = MetadataFactory::getScalarForType($hashmap['id']->getTypehint(), true);
95                    $property->setType(
96                        $scalarType->value
97                    );
98                    $doctrineType = $scalarType->toDoctrineType();
99                }
100            }
101        }
102
103        if (in_array(AutoIncrementTableInterface::class, $classType->getImplements())
104            || in_array(MixedStorageInterface::class, $classType->getImplements())) {
105            $generatedValue = true;
106            $nullable = false;
107        }
108
109        $hasIdAttribute = false;
110        $hasColumnAttribute = false;
111        foreach ($property->getAttributes() as $attribute) {
112            if (in_array($attribute->getName(), [Column::class, ManyToOne::class, OneToMany::class, ManyToMany::class])) {
113                $hasColumnAttribute = true;
114                break;
115            }
116            if ($attribute->getName() === GeneratedValue::class) {
117                $generatedValue = false;
118            }
119            if ($attribute->getName() === Id::class) {
120                $hasIdAttribute = true;
121            }
122        }
123        if (!$hasIdAttribute) {
124            $property->addAttribute(Id::class);
125        }
126        if (!$hasColumnAttribute) {
127            if ($doctrineType === null) {
128                $doctrineType = MetadataFactory::getScalarForType(
129                    ReflectionTypeFactory::createReflectionType($property->getType()),
130                    true
131                )->toDoctrineType();
132            }
133            $property->addAttribute(Column::class, ['type' => $doctrineType, 'nullable' => $nullable]);
134        }
135        if ($generatedValue) {
136            $property->addAttribute(GeneratedValue::class);
137        }
138    }
139
140    /**
141     * @return Generator<int, PromotedParameter|Property>
142     */
143    private function iterateProperties(ClassType $classType): Generator
144    {
145        foreach ($classType->getProperties() as $property) {
146            yield $property;
147        }
148        if ($classType->hasMethod('__construct')) {
149            foreach ($classType->getMethod('__construct')->getParameters() as $parameter) {
150                if ($parameter instanceof PromotedParameter) {
151                    yield $parameter;
152                }
153            }
154        }
155    }
156
157    private function patch(GeneratedCodeContext $generatedCodeContext, ClassType $classType): void
158    {
159        $classType->addAttribute(Entity::class);
160        $classType->addAttribute(HasLifecycleCallbacks::class);
161        $classType->addTrait('\\' . HasGeneralDoctrineFields::class);
162
163        // @see ClassTypeFactory
164        $originalClass = $classType->getComment();
165        if ($originalClass && class_exists($originalClass)) {
166            if (is_a($originalClass, RequiresRecalculatingInterface::class, true)) {
167                $classType->addTrait('\\' . RequiresDomainUpdate::class);
168            }
169        }
170
171        foreach ($this->iterateProperties($classType) as $property) {
172            $added = false;
173            $attributes = [];
174            foreach ($property->getAttributes() as $attribute) {
175                switch ($attribute->getName()) {
176                    case DecimalPropertyAttribute::class:
177                        $added = true;
178                        $arguments = $attribute->getArguments();
179                        $attributes[] = new Attribute(Column::class, ['nullable' => true, 'type' => 'decimal', 'precision' => $arguments[2] ?? 2]);
180                        break;
181                    case GetMethodAttribute::class:
182                    case PropertyAttribute::class:
183                        $added = true;
184                        if (in_array($property->getType(), ['DateTimeImmutable', '?DateTimeImmutable'])) {
185                            $attributes[] = new Attribute(Column::class, ['nullable' => true, 'type' => 'datetimetz_immutable']);
186                        } else {
187                            $arguments = $attribute->getArguments();
188                            if ($arguments[2] ?? false) {
189                                $attributes[] = new Attribute(Column::class, ['nullable' => true, 'type' => 'text']);
190                            } else {
191                                $attributes[] = new Attribute(Column::class, ['nullable' => true]);
192                            }
193                        }
194                        break;
195                    case DiscriminatorMappingAttribute::class:
196                        $added = true;
197                        $attributes[] = new Attribute(Column::class, ['type' => 'json']);
198                        break;
199                    case ManyToOneAttribute::class:
200                        $added = true;
201                        $targetEntity = $property->getType();
202                        $attributes[] = new Attribute(
203                            ManyToOne::class,
204                            [
205                                'targetEntity' => $targetEntity,
206                                'inversedBy' => $attribute->getArguments()[0],
207                            ]
208                        );
209                        $attributes[] = new Attribute(
210                            JoinColumn::class,
211                            [
212                                'nullable' => true,
213                                'onDelete' => 'CASCADE',
214                            ]
215                        );
216                        break;
217                    case OneToManyAttribute::class:
218                    case AclLinkAttribute::class:
219                        $added = true;
220                        $property->setType(Collection::class);
221                        if ($attribute->getName() === OneToManyAttribute::class) {
222                            $targetEntity = $attribute->getArguments()[1];
223                            $mappedByProperty = $generatedCodeContext->findParentProperty($targetEntity);
224                            $mappedByProperty ??= $attribute->getArguments()[0];
225                            $mappedByProperty ??= 'ref_' . $classType->getName();
226                        } else {
227                            $targetEntity = $attribute->getArguments()[0];
228                            $mappedByProperty = 'ref_' . $classType->getName();
229                        }
230                        $indexByProperty = $generatedCodeContext->findIndexProperty($targetEntity);
231                        if ($indexByProperty) {
232                            $attributes[] = new Attribute(OrderBy::class, [[$indexByProperty => 'ASC']]);
233                        }
234                        $attributes[] = new Attribute(
235                            OneToMany::class,
236                            [
237                                'cascade' => ['all'],
238                                'targetEntity' => $targetEntity,
239                                'mappedBy' => $mappedByProperty,
240                                'fetch' => 'EAGER',
241                                'indexBy' => $indexByProperty,
242                                'orphanRemoval' => true,
243                            ]
244                        );
245
246                        break;
247                    case OneToOneAttribute::class:
248                        $added = true;
249                        $targetEntity = $property->getType();
250                        // look for @ParentAttribute for inversedBy?
251                        $attributes[] = new Attribute(
252                            OneToOne::class,
253                            [
254                                'cascade' => ['all'],
255                                'targetEntity' => $targetEntity,
256                                'fetch' => 'EAGER',
257                                'orphanRemoval' => true,
258                            ]
259                        );
260                        break;
261                    case GetSearchIndexAttribute::class:
262                        $added = true;
263                        $property->setType(Collection::class);
264                        $searchTableName = strpos($classType->getName(), 'apie_resource__') === 0
265                            ? preg_replace('/^apie_resource__/', 'apie_index__', $classType->getName())
266                            : 'apie_index__' . $classType->getName();
267                        $searchTableName .= '_' . $property->getName();
268                        $searchTable = SearchIndex::createFor(
269                            $searchTableName,
270                            $classType->getName(),
271                            $property->getName(),
272                        );
273                        $generatedCodeContext->generatedCode->generatedCodeHashmap[$searchTableName] = $searchTable;
274                        $attributes[] = new Attribute(
275                            OneToMany::class,
276                            [
277                                'cascade' => ['all'],
278                                'targetEntity' => $searchTableName,
279                                'mappedBy' => 'parent',
280                                'orphanRemoval' => true,
281                            ]
282                        );
283                        $args = $attribute->getArguments();
284                        $args['arrayValueType'] = $searchTableName;
285                        $attribute = new Attribute($attribute->getName(), $args);
286                        $type = $property->getType();
287                        break;
288                    case OrderAttribute::class:
289                        $added = true;
290                        $type = 'text';
291                        if ($property->getType() === 'int') {
292                            $type = 'integer';
293                        }
294                        $attributes[] = new Attribute(Column::class, ['type' => $type]);
295                        break;
296                    case ParentAttribute::class:
297                        $added = true;
298                        $inversedBy = $generatedCodeContext->findInverseProperty($property->getType(), $classType->getName());
299                        $attributes[] = new Attribute(
300                            ManyToOne::class,
301                            ['targetEntity' => $property->getType(), 'inversedBy' => $inversedBy]
302                        );
303                        $attributes[] = new Attribute(
304                            JoinColumn::class,
305                            [
306                                'onDelete' => 'CASCADE',
307                            ]
308                        );
309                        break;
310                }
311                $attributes[] = $attribute;
312            }
313            if (!$added) {
314                $type = $property->getType();
315                switch ((string) $type) {
316                    case 'string':
317                        $attributes[] = new Attribute(Column::class, ['type' => 'text', 'nullable' => $property->isNullable()]);
318                        break;
319                    case 'float':
320                        $attributes[] = new Attribute(Column::class, ['type' => 'float', 'nullable' => $property->isNullable()]);
321                        break;
322                    case 'int':
323                    case '?int':
324                        $attributes[] = new Attribute(Column::class, ['type' => 'integer', 'nullable' => $property->isNullable()]);
325                        break;
326                    case 'array':
327                    case '?array':
328                        $attributes[] = new Attribute(Column::class, ['type' => 'json', 'nullable' => $property->isNullable()]);
329                        break;
330                }
331            }
332            $property->setAttributes($attributes);
333        }
334
335        $this->applyId($classType);
336    }
337}